package model

import (
	"fmt"
	"reflect"

	"github.com/stoewer/go-strcase"

	"github.com/iancoleman/orderedmap"

	"github.com/invopop/jsonschema"
)

type (
	Compose struct {
		Version  Version  `json:"version" bson:"version"`
		Services Services `json:"services" bson:"services"`
		Volumes  Volumes  `json:"volumes,omitempty" bson:"volumes"`
	}

	Service struct {
		Image       string       `json:"image" bson:"image"`
		Ports       Ports        `json:"ports,omitempty" bson:"ports"`
		Environment Environment  `json:"environment,omitempty" bson:"environment"`
		Labels      Labels       `json:"labels,omitempty" bson:"labels"`
		Volumes     VolumeMounts `json:"volumes,omitempty" bson:"volumes"`
		Deployment  Deployment   `json:"deploy,omitempty" bson:"deploy"`
	}

	ServiceName string
	Services    map[ServiceName]Service

	Version string

	Deployment struct {
		Resources Resources `json:"resources,omitempty" bson:"resources"`
	}

	Resources struct {
		Reservations Resource `json:"reservations,omitempty" bson:"reservations"`
		Limits       Resource `json:"limits,omitempty"       bson:"limits"`
	}

	Resource struct {
		Cpu    Cpu `json:"cpus"   bson:"cpus"`
		Memory Ram `json:"memory" bson:"memory"`
	}
)

const (
	ServiceDefinitionName      = "service"
	ListOrDictDefinitionName   = "list_or_dict"
	ResourcesDefinitionName    = "resources"
	LimitsDefinitionName       = "limits"
	ReservationsDefinitionName = "reservations"
	CPUDefinitionName          = "cpus"
	MemoryDefinitionName       = "memory"

	DefinitionsRefTemplate = "#/$defs/%s"
)

var (
	reflector = &jsonschema.Reflector{
		ExpandedStruct: true,
		Namer:          kebabCase,
	}
)

func kebabCase(t reflect.Type) string {
	return strcase.KebabCase(t.Name())
}

func Schema() *jsonschema.Schema {
	root := reflector.Reflect(&Compose{})
	embedSchema(root, ServiceDefinitionName, &Service{})
	embedSchema(root, ListOrDictDefinitionName, &ListOrDict{})
	embedSchema(root, ResourcesDefinitionName, &Resources{})
	embedSchema(root, ReservationsDefinitionName, &Resource{})
	embedSchema(root, LimitsDefinitionName, &Resource{})
	embedSchema(root, CPUDefinitionName, &Cpu{})
	embedSchema(root, MemoryDefinitionName, &Ram{})

	return root
}

func embedSchema(root *jsonschema.Schema, definition string, from interface{}) {
	embedded := reflector.Reflect(from)
	embedded.ID = jsonschema.EmptyID
	embedded.Version = ""

	for embeddedDefinitionName, embeddedDefinition := range embedded.Definitions {
		root.Definitions[embeddedDefinitionName] = embeddedDefinition
	}

	embedded.Definitions = jsonschema.Definitions{}

	root.Definitions[definition] = embedded
}

func (Version) JSONSchema() *jsonschema.Schema {
	return &jsonschema.Schema{
		Type:        "string",
		Description: "Version of the Compose specification used. Tools not implementing required version MUST reject the configuration file.",
	}
}

func (Services) JSONSchema() *jsonschema.Schema {
	return &jsonschema.Schema{
		Type: "object",
		PatternProperties: map[string]*jsonschema.Schema{
			"^[a-zA-Z0-9._-]+$": {
				Ref: fmt.Sprintf(DefinitionsRefTemplate, ServiceDefinitionName),
			},
		},
		AdditionalProperties: jsonschema.FalseSchema,
	}
}

func (Deployment) JSONSchema() *jsonschema.Schema {
	properties := orderedmap.New()

	properties.Set("resources", &jsonschema.Schema{
		Ref: fmt.Sprintf(DefinitionsRefTemplate, ResourcesDefinitionName),
	})

	return &jsonschema.Schema{
		Type:                 "object",
		Properties:           properties,
		AdditionalProperties: jsonschema.TrueSchema,
		//...
	}
}

func (Resources) JSONSchema() *jsonschema.Schema {
	properties := orderedmap.New()

	properties.Set("reservations", &jsonschema.Schema{
		Ref: fmt.Sprintf(DefinitionsRefTemplate, ReservationsDefinitionName),
	})

	properties.Set("limits", &jsonschema.Schema{
		Ref: fmt.Sprintf(DefinitionsRefTemplate, LimitsDefinitionName),
	})

	return &jsonschema.Schema{
		Type:                 "object",
		Properties:           properties,
		AdditionalProperties: jsonschema.FalseSchema,
		PatternProperties: map[string]*jsonschema.Schema{
			"^x-": {},
		},
	}
}

func (Resource) JSONSchema() *jsonschema.Schema {
	properties := orderedmap.New()

	properties.Set("cpus", &jsonschema.Schema{
		Ref: fmt.Sprintf(DefinitionsRefTemplate, CPUDefinitionName),
	})
	properties.Set("memory", &jsonschema.Schema{
		Ref: fmt.Sprintf(DefinitionsRefTemplate, MemoryDefinitionName),
	})
	return &jsonschema.Schema{
		Type:                 "object",
		Properties:           properties,
		AdditionalProperties: jsonschema.TrueSchema,
	}
}
